feat(types): add top-level skills option to ClaudeAgentOptions#804
feat(types): add top-level skills option to ClaudeAgentOptions#804jsham042 wants to merge 5 commits intoanthropics:mainfrom
skills option to ClaudeAgentOptions#804Conversation
Adds a `skills: list[str] | None` field to ClaudeAgentOptions that mirrors the existing field on AgentDefinition. When set, the SDK automatically: - Adds `Skill` (or `Skill(name)` patterns for specific names) to the `--allowedTools` CLI flag. - Defaults `setting_sources` to `["user", "project"]` when not already configured, so installed SKILL.md files are discovered. Previously, enabling skills required both "Skill" in allowed_tools and an explicit setting_sources list — a footgun the SDK can easily remove. The existing `allowed_tools` and `setting_sources` fields are unchanged and still take precedence when the caller sets them explicitly. The options object itself is never mutated.
|
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #804 +/- ##
=======================================
Coverage ? 84.47%
=======================================
Files ? 14
Lines ? 2506
Branches ? 0
=======================================
Hits ? 2117
Misses ? 389
Partials ? 0 ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
In addition to translating `skills` into `Skill(name)` allowedTools entries, also forward the list on the SDK `initialize` control request. A supporting CLI can use this to filter which skills are loaded into the system prompt (not just permission-gated). Older CLIs ignore unknown initialize fields, so this is forward-compatible.
|
CLI-side companion that honors |
Unlisted skills are hidden from the listing and blocked at the Skill tool, but their files remain readable via Read/Bash. Document the boundary and the alternatives (local plugin, deny rules) for users who need hard isolation.
API design refinement: skills is now the one place to enable skills
(users should not put 'Skill' in allowed_tools directly).
None - skills off (default)
'all' - every discovered skill
[name,...] - named subset only
[] - degenerate subset; setting_sources still defaults but no
Skill entries are added (natural list semantics)
Type widened to list[str] | Literal['all'] | None. Transport, query
init, docstring, and tests updated.
E2E Test ResultsRan the Python SDK PR branch ( Test script"""E2E proof for ClaudeAgentOptions.skills (PR #804 + CLI PR #27911)."""
import asyncio
import shutil
import sys
import tempfile
import textwrap
from pathlib import Path
from claude_agent_sdk import ClaudeAgentOptions, ResultMessage, query
CLI_PATH = str(
Path.home()
/ "code/claude-cli-internal/build-ant-native/@anthropic-ai/claude-cli-native-darwin-arm64/cli"
)
def make_project() -> Path:
root = Path(tempfile.mkdtemp(prefix="e2e-skills-"))
for name, desc in [
("greeter", "Say hello in a friendly way."),
("calculator", "Add two numbers together."),
]:
d = root / ".claude" / "skills" / name
d.mkdir(parents=True)
(d / "SKILL.md").write_text(
textwrap.dedent(f"""\
---
name: {name}
description: {desc}
---
# {name}
{desc}
""")
)
return root
async def run_case(label: str, cwd: Path, skills) -> str:
opts = ClaudeAgentOptions(
cwd=str(cwd),
skills=skills,
setting_sources=["project"],
allowed_tools=[],
max_turns=1,
cli_path=CLI_PATH,
system_prompt={"type": "preset", "preset": "claude_code"},
)
out = []
async with asyncio.timeout(90):
async for msg in query(
prompt="List the names of every skill available to you in this session, one per line. If you have no skills, say 'NONE'.",
options=opts,
):
if isinstance(msg, ResultMessage):
out.append(msg.result or "")
return f"[{label}] skills={skills!r}\n" + "\n".join(out).strip()
async def main() -> int:
cwd = make_project()
print(f"project: {cwd}")
try:
for label, skills in [
("case1-none", None),
("case2-all", "all"),
("case3-subset", ["greeter"]),
]:
print(await run_case(label, cwd, skills))
print("-" * 40)
finally:
shutil.rmtree(cwd, ignore_errors=True)
return 0
if __name__ == "__main__":
sys.exit(asyncio.run(main()))OutputSummary
|
'all' and omitted both mean 'no filter' at the wire level, so only send the field when it is an explicit list. Keeps the CLI control schema as a plain string[] (which the zod-to-proto pipeline can represent) while the 'all' sentinel remains at the Python API surface for ergonomics.
|
My current feature requires this PR |
- anthropics#806: setting_sources=[] truthiness fix - anthropics#803: betas=[]/plugins=[] truthiness fix - anthropics#786: ThinkingBlock missing signature crash fix - anthropics#790: suppress ProcessError when result already received - anthropics#658: capture real stderr in ProcessError - anthropics#791: suppress stale task notifications between turns - anthropics#763: guard malformed CLAUDE_CODE_STREAM_CLOSE_TIMEOUT env var - anthropics#805: delete_session() cascades subagent transcript dir - anthropics#804: top-level skills option on ClaudeAgentOptions - anthropics#691: PostCompact hook event type support 479 tests passing, mypy clean, ruff clean. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Summary
Adds
skills: list[str] | Literal["all"] | NonetoClaudeAgentOptionsas the single place to enable Skills for the main session, mirroring the existingAgentDefinition.skillsfield for subagents (added in #684).Today, enabling Skills requires two non-obvious steps in unrelated fields:
With this change:
Users no longer put
"Skill"inallowed_tools. The old way still works and is unchanged.Behavior
When
skillsis set, two things happen:1. CLI flags (works on all CLI versions): the transport layer computes effective
allowed_toolsandsetting_sourcesat command-build time:"all"→ appends bareSkillto--allowedTools.[name, ...]→ appendsSkill(name)for each entry.setting_sourcesisNone→ defaults to["user", "project"]so installed SKILL.md files are discovered.allowed_toolsentries are preserved, explicitsetting_sourcestake precedence, and duplicateSkill(name)entries are not re-added.2.
initializecontrol request (forward-compatible): the value is also forwarded as{"skills": ...}on the SDKinitializerequest. A supporting CLI uses this to filter which Skills are loaded into the system prompt (not just permission-gated). Older CLIs ignore unknown initialize fields, so this degrades to permission-layer gating only.skills=None(default) is a complete no-op.Scope: context filter, not sandbox
skills=[...]controls what the model sees and can invoke. Unlisted Skills are hidden from the listing andSkill(name)calls for them are not inallowedTools. It does not restrict filesystem access: a session withRead/Bashcan still open.claude/skills/**directly. This matches the existingAgentDefinition.skillssemantics for subagents. For hard isolation, use a local plugin withsetting_sources=None, or permission deny rules. The docstring and the agent-sdk/skills docs page (companion CLI PR) call this out explicitly.Companion PR
CLI-side prompt filtering: anthropics/claude-cli-internal#27911
Related issues
Test plan
tests/test_transport.py— 8 tests covering None / "all" / named / [] / merge / override / no-mutation / idempotence.tests/test_query.py— 2 tests coveringskillspresence/absence in theinitializepayload.All 469 tests pass;
ruffandmypyclean.